/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
import com.bourre.commands.Delegate;
import com.bourre.core.HashCodeFactory;
import com.bourre.log.PixlibStringifier;
import com.bourre.structures.Range;
import com.bourre.transitions.IFrameListener;
import com.kairos.engine.RTEvent;
import com.kairos.engine.RTObject;
import com.kairos.log.KairosDebug;
import com.kairos.utils.monitor.MonitorField;

import flash.display.BitmapData;
import flash.geom.Point;
import flash.geom.Rectangle;

/**
 * The {@code com.kairos.utils.Monitor} tool is a performance profiler, it display
 * statistic on RTBeacon runs, framerate and memory usage.
 * 
 * <p>Use the {@code MonitorFieldsList} utility class to add the fields you
 * needs in the monitor.</p>
 * 
 * @author Cédric Néhémie
 * @example	Adding a statistic monitor to your project and adding two fields in it.
 * 
 * <code>var stats : com.kairos.utils.Monitor = new com.kairos.utils.Monitor();
 * 
 * stats.addField( MonitorFieldsList.STEP );
 * stats.addField( MonitorFieldsList.AVERAGE_STEP );
 * 
 * addChild( stats );
 * 
 * stats.start();
 * </code>
 *  
 * You can also specify the size of the monitor's graph by passing it to
 * the constructor.
 * 
 * <code>var stats : com.kairos.utils.Monitor = new com.kairos.utils.Monitor( 150, 200 );
 * 
 * stats.addField( MonitorFieldsList.STEP );
 * stats.addField( MonitorFieldsList.AVERAGE_STEP );
 * 
 * addChild( stats );
 * 
 * stats.start();
 * </code>
 */
class com.kairos.utils.Monitor extends RTObject implements IFrameListener
{				
	/**
	 * Color of the background of the monitor.
	 */
	public static var COLOR_BACKGROUND : Number = 0x111111;
	
	/**
	 * Color of the alternate background of monitor's fields.
	 */
	public static var COLOR_BACKGROUND_ALTERNATE1 : Number = 0x111111;
	
	/**
	 * Color of the second alternate background of monitor's fields.
	 */
	public static var COLOR_BACKGROUND_ALTERNATE2 : Number = 0x222222;
	
	/**
	 * Color of the background of the monitor's graph.
	 */
	public static var COLOR_GRAPH_BACKGROUND : Number = 0x000000;
	
	/**
	 * Color of the borders of the monitor's graph.
	 */
	public static var COLOR_GRAPH_BORDER : Number = 0x999999;
	
	/**
	 * Color of the values of the fields.
	 */
	public static var COLOR_VALUE : Number = 0xcccccc;
	
	// Graphical formatting values 
	private static var __MARGIN : Number = 5;
	private static var __LINE_SPACING : Number = 0;
	private static var __LEFT_PANEL_SIZE : Number = 50;
	
	// Runner content	
	private var _aFields : Array;
	private var _aTextFields : Array;
	private var _aTextFieldsLabel : Array;
	private var _aBackgrounds : Array;
	private var _aButtons : Array;
	
	// Graph entity
	private var __nBMPWidth : Number;
	private var __nBMPHeight : Number;
	private var __bmpGraph : BitmapData;
	private var __bmpEmpty : BitmapData;
	private var __bmpTemp : BitmapData;
	
	// UI Entity
	private var __mcBackground : MovieClip;
	private var __mcBorder : MovieClip;
	private var __oGraphHolder : MovieClip;
	
	private var __mcRoot : MovieClip;

	private var _aKeys : Array;

	private var _nID : Number;
	
	/**
	 * Creates a new {@code com.kairos.utils.Monitor} object with a graph of size
	 * <code>width * height</code>.
	 * 
	 * <p>At creation the monitor don't contains any field, use the {@code addField}
	 * to add monitoring field in the monitor. You could find many field in the 
	 * {@code MonitorFieldsList}.</p>
	 *  
	 * @param	target	{@code MovieClip} target for the monitor.
	 * @param	width	{@code Number} of pixel of the graph's width.
	 * @param 	height	{@code Number} of pixel of the graph's height.
	 */
	public function Monitor( target : MovieClip, width : Number, height : Number)
	{
		if( isNaN( width ) || width <= 0 )
			width = 200;
		if( isNaN( height ) || height <= 0 )
			height = 100;
		
		__nBMPWidth = width;
		__nBMPHeight = height;
		
		__mcRoot = target;
		_nID = 0;
		
		_aFields = new Array();
		_aTextFields = new Array();
		_aTextFieldsLabel = new Array();
		_aBackgrounds = new Array();
		_aButtons = new Array();
		_aKeys = new Array();
		
		_createUI();
	}
	
	/**
	 * Adds the passed-in {@code com.kairos.utils.MonitorField} into the monitor.
	 * 
	 * <p>Returns {@code true} if the field have been added
	 * in the monitor, {@code false} otherwise.</p>
	 * 
	 * @param  	o	{@code com.kairos.utils.MonitorField} to add into the monitor.	
	 * @return 	{@code true} if the field have been added into
	 * 			the monitor, {@code false} otherwise.
	 */
	public function addField ( o : MonitorField ) : Boolean
	{
		if( o == null )
		{
			KairosDebug.WARN( "Attempt to add a null field in " + this );
			return false;
		}
		else if( !containsField( o ) )
		{	
			var key : Number = HashCodeFactory.getKey(o);
					
			o.setDisplayHeight( __nBMPHeight );
			_aFields[ key ] = o ;
			_aKeys.push( key );
			
			if( o.isLabel() )
			{								
				var bg : MovieClip = __createBackground( _aFields.length%2 == 0 ? COLOR_BACKGROUND_ALTERNATE2 : COLOR_BACKGROUND_ALTERNATE1 );
				_aBackgrounds[ key ] = bg;
				
				var btn : MovieClip = __mcRoot.createEmptyMovieClip( "btn" + o.getColor() , __mcRoot.getNextHighestDepth() );
				btn.useHandCursor = false;
				_drawButton( btn, o.getColor(), false );
				if( o.isCurve() ) btn.onRelease = Delegate.create( this, _toggleField, btn, o );
				_aButtons[ key ] = btn;
				
				var txt : TextField = __createTextField( COLOR_VALUE );	
				_aTextFields[ key ] = txt;
					
				var txt2 : TextField = __createTextField( o.getColor() );		
				_aTextFieldsLabel[ key ] = txt2;
				txt2.text = o.getName();
				
				_fitToBitmap();		
			}
			return true;
		}
		else return false;
	}
	
	/**
	 * Removes the passed-in {@code com.kairos.utils.MonitorField} from the monitor.
	 * 
	 * <p>Returns {@code true} if the field have been removed from
	 * the monitor, {@code false} otherwise.</p>
	 * 
	 * @param  	o	{@code com.kairos.utils.MonitorField} to remove from the monitor.	
	 * @return 	{@code true} if the field have been removed from
	 * 			the monitor, {@code false} otherwise.
	 */
	public function removeField ( o : MonitorField ) : Boolean
	{
		if( o == null )
		{
			KairosDebug.WARN( "Attempt to remove a null field from " + this );
			return false;
		}
		else if( !containsField( o ) )
			return false;
		
		var id : Number = _aFields.indexOf( o );
		var key : Number = HashCodeFactory.getKey( o );
		
		_aFields[ key ] = null;		
		
		if( o.isLabel() )
		{
			_aTextFieldsLabel[ key ].removeMovieClip();
			_aTextFields[ key ].removeMovieClip();
			_aBackgrounds[ key ].removeMovieClip();
			_aButtons[ key ].removeMovieClip();
			
			_aTextFieldsLabel[ key ] = null;
			_aTextFields[ key ] = null;
			_aBackgrounds[ key ] = null;
			_aButtons[ key ] = null;
			
			_fitToBitmap();	
		}
		
		var l : Number = _aKeys.length;
		while( --l -(-1) )
		{
			if( _aKeys[l] == key )
			{
				_aKeys.splice( l, 1 );
				break;
			}
		}		
		return true;
	}
	
	/**
	 * Returns {@code true} if the {@code com.kairos.utils.MonitorField}
	 * exist in the monitor.
	 * 
	 * @param  	o	{@code com.kairos.utils.MonitorField} to verify if it's in the monitor.	
	 * @return 	{@code true} if the field exist in the monitor,
	 * 			{@code false} otherwise.
	 */
	public function containsField ( o : MonitorField ) : Boolean
	{
		return (_aFields[ HashCodeFactory.getKey(o) ] != null );
	}
			
	/**
	 * Process all animations according to received value
	 * from the {@code RTBeacon}.
	 * 
	 * @param	e	{@code RTEvent} passed by the {@code RTBeacon}.
	 */
	public function onEnterFrame():Void
	{
		var evt : RTEvent = RTEvent ( arguments[ 0 ] );
		var step : Number = evt.getStep();

		__move();
		
		var l : Number = _aKeys.length;
		for( var i : Number = 0 ; i < l ; i++ )
		{
			var key : Number = _aKeys[ i ];
			var f : MonitorField = MonitorField( _aFields[ key ] );
			var t : TextField = _aTextFields[ key ];
			
			f.registerValue( evt );
			if( f.isLabel() ) t.text = f.getFormattedValue();
			if( f.isCurve() && f.isEnabled() ) __drawLine( __nBMPWidth-1, f.getDifference(), f.getColor() );
		}
	}
		
	/**
	 * Clears all bitmaps used by the object.
	 */
	public function dispose () : Void
	{
		__bmpEmpty.dispose();
		__bmpGraph.dispose();
		__bmpTemp.dispose();
	}
			
	/**
	 * Returns the String representation of the object. 
	 * 
	 * @return	{@code String} representation of the object. 
	 */	
	public function toString() : String
	{
		return PixlibStringifier.stringify( this );
	}
	
	/**
	 * Builds the whole graphics for the monitor. 
	 */
	private function _createUI() : Void
	{			
		__bmpEmpty = new BitmapData ( __nBMPWidth, __nBMPHeight, true, 0x00000000 );
		__bmpTemp  = new BitmapData ( __nBMPWidth, __nBMPHeight, true, 0x00000000 );
		__bmpGraph = new BitmapData ( __nBMPWidth, __nBMPHeight, true, 0x00000000 );		
		
		__mcBackground = __createBackground( COLOR_BACKGROUND );
		__mcBorder = __createBorder( COLOR_GRAPH_BACKGROUND, COLOR_GRAPH_BORDER );
		
		__oGraphHolder = __mcRoot.createEmptyMovieClip( "graph", __mcRoot.getNextHighestDepth() );
		__oGraphHolder.attachBitmap( __bmpGraph, 0 );	
	}
	
	/**
	 * Adjusts all graphics object to the size of the graph specified
	 * in the monitor constructor.
	 */
	private function _fitToBitmap () : Void
	{
		var h : Number = 16;
		var y : Number = __nBMPHeight + __MARGIN * 2;
		var l : Number = _aKeys.length;
		
		for( var i : Number = 0 ; i < l ; i++ )
		{
			var key : Number = _aKeys [ i ];
			var field : MonitorField = _aFields[ key ];
			
			if( field.isLabel() )
			{
				var txt1 : TextField = _aTextFields[ key ];
				var txt2 : TextField = _aTextFieldsLabel[ key ];
				var bg : MovieClip = _aBackgrounds[ key ];
				var btn : MovieClip = _aButtons[ key ];
				
				btn._x = __MARGIN + 3;
				btn._y = y + 3;
				
				txt2._x = __MARGIN + 16 ;
				txt2._y = y;
				txt1._x = txt2._x * 2 + __nBMPWidth/2;
				txt1._y = y;
				bg._x = __MARGIN;
				bg._y = y;
				bg._width = __nBMPWidth;
				bg._height = h;
				
				y += __LINE_SPACING + h;
			}
		}
		
		__mcBorder._x = __MARGIN;
		__mcBorder._y = __MARGIN;
		__mcBorder._width = __nBMPWidth;
		__mcBorder._height = __nBMPHeight;
		
		__mcBackground._width = __nBMPWidth + __MARGIN * 2;
		__mcBackground._height = y + __MARGIN;
		
		__oGraphHolder._x = __MARGIN;
		__oGraphHolder._y = __MARGIN;
	}
	
	/**
	 * Realizes the scroll of the graph bitmap
	 */		
	private function __move() : Void
	{
		var rc1 : Rectangle = new Rectangle ( 0, 0, __nBMPWidth , __nBMPHeight );
		var rc2 : Rectangle = new Rectangle ( 1, 0, __nBMPWidth - 1, __nBMPHeight );
		var pt1 : Point = new Point ( 0, 0 );
		
		__bmpTemp.copyPixels( __bmpGraph, rc1, pt1 );
		__bmpGraph.copyPixels( __bmpEmpty, rc1, pt1 );
		__bmpGraph.copyPixels( __bmpTemp, rc2, pt1 );
	}
	
	/**
	 * Draws a vertical line at {@code x} in {@code r}
	 * 
	 * @param 	x	X position where drawing the line.
	 * @param 	r 	Y values covered by the line.
	 * @param 	col	Color of the line.
	 */
	private function __drawLine ( x:Number, r : Range, col : Number ) : Void 
	{
		var ys : Number;
		var ye : Number;
		if( r.min > r.max )
		{
			ys = r.max;
			ye = r.min;
		}
		else
		{
			ys = r.min;
			ye = r.max;
		}
		while ( ys++ <= ye )
		{
			__bmpGraph.setPixel32( x, ys, col );
		}						
	}
	
	/**
	 * Creates and returns a new {@code TextField} of color {@code col}
	 * 
	 * @param	col	Color of the {@code TextField}
	 * @return  The new {@code TextField}
	 */
	private function __createTextField ( col : Number ) : TextField
	{
		var id : Number = _nID++;
		__mcRoot.createTextField( "field" + id, __mcRoot.getNextHighestDepth(), 0, 0, Math.round( __nBMPWidth / 2 ), 16 );
		var txt : TextField = __mcRoot[ "field" + id ];
		var format : TextFormat = new TextFormat( "Verdana", 10, col );
		//txt.autoSize = TextFieldAutoSize.LEFT;
		txt.selectable = false;
		txt.setNewTextFormat ( format );
		txt.text = "----";
		return txt;
	}
	
	/**
	 * Creates and returns a {@code Shape} object witch represent the border 
	 * of the bitmap graph.
	 * 
	 * @return	A new {@code Shape} object. 
	 */
	private function __createBorder( col1 : Number, col2:Number ) : MovieClip
	{
		var gfx : MovieClip = __mcRoot.createEmptyMovieClip( "border" + col1 + "_" + col2 , __mcRoot.getNextHighestDepth() );
		
		gfx.beginFill( col1 );
		gfx.lineStyle( 0, col2 );
		gfx.moveTo( 0, 0 );
		gfx.lineTo( 0, 10 );
		gfx.lineTo( 10, 10 );
		gfx.lineTo( 10, 0 );
		gfx.lineTo( 0, 0 );
		gfx.endFill();

		return gfx;	
	}
	/**
	 * Creates and returns a {@code Shape} object witch represent the
	 * background of the monitor.
	 * 
	 * @return	A new {@code Shape} object. 
	 */
	private function __createBackground( col : Number ) : MovieClip
	{
		var gfx : MovieClip = __mcRoot.createEmptyMovieClip( "bg" + col , __mcRoot.getNextHighestDepth() );
		
		gfx.beginFill( col );
		gfx.moveTo( 0, 0 );
		gfx.lineTo( 0, 10 );
		gfx.lineTo( 10, 10 );
		gfx.lineTo( 10, 0 );
		gfx.lineTo( 0, 0 );
		gfx.endFill();

		return gfx;	
	}
	
	/**
	 * Paints button on passed-in {@code target} to represent "Toggle button" for 
	 * graph view.
	 * 
	 * @param target The {@code MovieClip} paint target.
	 * @param color Color to use to draw the button
	 * @param hole Draw a hole ( {@code true} or a not {@code false}
	 */
	private function _drawButton( target : MovieClip, color : Number, hole : Boolean ) : Void 
	{
		target.clear();
		
		if( hole )
		{
			target.lineStyle( 1, color );
			target.beginFill( 0x000000, 0 );
		}
		else target.beginFill( color );
		
		target.moveTo( 0, 0 );
		target.lineTo( 0, 10 );
		target.lineTo( 10, 10 );
		target.lineTo( 10, 0 );
		target.lineTo( 0, 0 );
	}
	
	/**
	 * Field button callback.
	 * 
	 * <p>Switch button and field state.
	 * 
	 * @param mc {@code MovieClip} target
	 * @param field {@code MonitorField} target
	 */
	private function _toggleField( mc : MovieClip, field : MonitorField ) : Void 
	{
		field.setEnabled( !field.isEnabled() );
		
		_drawButton( mc, field.getColor(), !field.isEnabled() );
	}
	
}